package online.model.session;

import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

import javax.servlet.http.HttpServletRequest;

import online.filter.FilterUtil;
import online.model.ModelUtil;
import online.model.UniModel;
import online.model.UniModelImpl;
import core.config.Env;
import core.util.ArrayUtil;
import core.util.DateUtil;

/**
 * アクションパラメタ
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class ActionParameter implements Serializable {

	/** トークン */
	public static final String TAG_TOKEN = "TOKEN";

	/** serialVersionUID */
	private static final long serialVersionUID = -3167940599445719054L;

	/** トークンアルゴリズム */
	private static final String TOKEN_ALGORITHM = "Token.Algorithm";

	/** 画面ID */
	private final String gid;
	/** アクションID */
	private final String aid;
	/** 初期時フラグ */
	private final boolean first;
	/** リモートアドレス */
	private final String remote;
	/** クエリ */
	private final String query;
	/** 汎用データモデル */
	private final UniModel um;
	/** パラメタマップ */
	private final Map<String, String[]> param;
	/** 共通項目 */
	private final Map<String, String> comm;

	/** 保存アクションセション */
	private final SessionStore store;

	/** トークン */
	private String token = null;
	/** ロールバックフラグ */
	private boolean rollbacked = false;
	/** アクションセション設定済フラグ */
	private boolean actionSession = false;
	/** 保存済消滅 */
	private boolean expiration = false;

	/**
	 * コンストラクタ
	 *
	 * @param request サーブレットリクエスト
	 * @param gamen 画面ID
	 * @param common 共通項目
	 * @param map パラメタマップ
	 * @param model 汎用モデル
	 */
	public ActionParameter(final HttpServletRequest request, final String gamen,
					final Map<String, String> common, final Map<String, String[]> map,
					final UniModel model) {
		this.first = model.getDateTime() == null;
		this.gid = gamen;
		this.param = map;
		this.remote = request.getRemoteAddr();
		this.store = populate(request);
		this.token = this.store.getToken();
		this.query = FilterUtil.removeSid(FilterUtil.getQueryString(request));
		this.comm = common;
		this.um = model;
		if (this.first) {
			this.um.setDateTime(DateUtil.getDateTime());
			this.aid = getAidFromParam(map, request.getMethod());
		} else {
			this.aid = "get";
		}
		setCommon();
	}

	/**
	 * 汎用モデル化
	 * @param ap ActionParameter
	 * @return 汎用モデル
	 */
	public static UniModel getUniModelToSet(final ActionParameter ap) {
		return ap == null ? new UniModelImpl(true) : new UniModelImpl(ap.um, true);
	}

	/**
	 * 保存アクションセション取得
	 * @param request サーブレットリクエスト
	 * @return 保存アクションセション
	 */
	private SessionStore populate(final HttpServletRequest request) {
		SessionStore ret = FilterUtil.getSessionAttribute(request);
		return ret != null ? ret : new SessionStore(null, null, false, null, null, null);
	}

	/**
	 * 日時取得
	 *
	 * @return 日時
	 */
	public Timestamp getDateTime() {
		return this.um.getDateTime();
	}

	/**
	 * IPアドレス取得
	 *
	 * @return IPアドレス
	 */
	public String getIp() {
		return this.remote;
	}

	/**
	 * 画面ID取得
	 * @return 画面ID
	 */
	public String getGid() {
		return this.gid;
	}

	/**
	 * アクションID取得
	 *
	 * @return アクションID
	 */
	public String getAid() {
		return this.aid;
	}

	/**
	 * 表示処理判断
	 *
	 * @return 表示処理時 true を返す。
	 */
	public boolean isGet() {
		return "get".equalsIgnoreCase(this.aid);
	}

	/**
	 * 初期処理確認
	 *
	 * @return 初期処理時 true を返す。
	 */
	public boolean isFirst() {
		return this.first;
	}

	/**
	 * ロールバック判断
	 *
	 * @return ロールバックした場合 true を返す。
	 */
	public boolean isRollbacked() {
		return this.rollbacked;
	}

	/**
	 * ロールバック設定
	 *
	 */
	public void setRollback() {
		this.rollbacked = true;
	}

	/**
	 * Token判断
	 *
	 * @return 同一の場合 true を返す。
	 */
	public boolean isSameToken() {
		try {
			return Objects.equals(getParameter(TAG_TOKEN), this.token);
		} finally {
			removeParameter(TAG_TOKEN);
		}
	}

	/**
	 * Token設定
	 */
	public void setToken() {
		if (this.token == null) {
			try {
				this.token = toHashString(String.valueOf(ThreadLocalRandom.current().nextDouble())
						+ getDateTime().getTime() + this.remote);
			} finally {
				removeParameter(TAG_TOKEN);
			}
		}
	}

	/**
	 * 文字列のダイジェストを16進文字列で返却する。
	 *
	 * @param str 元文字列
	 * @return ダイジェスト
	 */
	private static String toHashString(final String str) {
		try {
			MessageDigest md = MessageDigest.getInstance(Env.getEnv(TOKEN_ALGORITHM, "SHA-256"));
			// バイト配列からダイジェストを計算する
			md.update(str.getBytes(StandardCharsets.ISO_8859_1));
			return toStringDigest(md.digest());
		} catch (final NoSuchAlgorithmException ex) {
			throw new IllegalStateException(ex);
		}
	}

	/**
	 * バイト配列を文字列に変換する。
	 *
	 * @param digest バイト配列
	 * @return 変換後文字列
	 */
	private static String toStringDigest(final byte[] digest) {
		// 返却値
		StringBuilder sb = new StringBuilder();
		for (final byte d : digest) {
			sb.append(String.format("%02x", Byte.valueOf(d)));
		}
		return sb.toString();
	}

	/**
	 * トークン取得
	 * @return トークン
	 */
	public String getToken() {
		return this.token;
	}

	/**
	 * トークン削除
	 */
	public void removeToken() {
		this.token = null;
		if (this.um != null) {
			this.um.remove(TAG_TOKEN);
		}
	}

	/**
	 * セション存在確認
	 *
	 * @param set 存在確認項目名集合
	 * @return 存在する場合 true を返す。
	 */
	public boolean hasItem(final Set<String> set) {
		// 項目存在チェック
		if (this.store.getUniModel() != null) {
			Set<String> rsv = this.store.getUniModel().getReservedKeySet();
			return rsv.containsAll(set);
		}
		return false;
	}

	/**
	 * クエリ文字列取得
	 * @return 初期時、セション期限切れ時の場合、現在のクエリ文字列を返す。
	 * それ以外の場合、セション開始時のクエリ文字列を返す。
	 */
	private String getQueryString() {
		if (this.expiration || this.store.getQueryString() == null) {
			return this.query;
		}
		return this.store.getQueryString();
	}

	/**
	 * クエリ文字列存在確認
	 * @return 存在した場合 true を返す。
	 */
	public boolean hasQueryString() {
		return !Objects.toString(getQueryString(), "").isEmpty();
	}

	/**
	 * 保存値設定
	 */
	public void copyReserved() {
		if (this.store.getUniModel() != null) {
			for (final String key : this.store.getUniModel().getReservedKeySet()) {
				if (ModelUtil.canSet(key) && !this.um.containsKey(key)) {
					this.store.getUniModel().copyValueTo(this.um, key);
				}
			}
		}
		this.actionSession = true;
	}

	/**
	 * 保存済消滅設定
	 */
	public void expireReserved() {
		this.expiration = true;
	}

	/**
	 * 退避処理
	 *
	 * @param request サーブレットリクエスト
	 * @return 退避した場合 true を返す。
	 */
	public boolean evacuate(final HttpServletRequest request) {
		// 初回時
		if (FilterUtil.hasSessionAttribute(request) || !this.rollbacked) {
			SessionStore ss = new SessionStore(this.um, this.param, this.rollbacked,
							getQueryString(), this.gid, this.token);
			if (FilterUtil.setSessionAttribute(request, ss)) {
				if (!this.expiration) {
					rereserve();
				}
				return true;
			}
		}
		return false;
	}

	/**
	 * 前回継続
	 * @param val メッセージ削除フラグ
	 */
	public void last(final boolean val) {
		setPreviousParameterMap();
		if (this.store.getUniModel() != null) {
			this.um.clear();
			boolean bool = permitted(val);
			for (final String key : this.store.getUniModel().keySet()) {
				if (bool || !ModelUtil.isMsgKey(key)) {
					this.store.getUniModel().putValueTo(this.um, key);
				}
			}
		}
		rereserve();
		setCommon();

		this.rollbacked = this.store.isRollback();
	}

	/**
	 * 許可確認
	 * @param val メッセージ削除フラグ
	 * @return 許可されてる場合 true を返す。
	 */
	private boolean permitted(final boolean val) {
		String owner = this.store.getGid();
		return !val || Objects.toString(owner, "").isEmpty() || owner.equals(this.gid);
	}

	/**
	 * ロールバック
	 * @param item ロールバックモデル項目名
	 */
	public void rollback(final String item) {
		if (!this.rollbacked) {
			if (isGet()) {
				setPreviousParameterMap();
			}
			setPreviousModel(item);
			setCommon();

			setRollback();
		}
	}

	/**
	 * リトライ
	 */
	public void retry() {
		this.um.clear();

		copyReserved();

		setCommon();
	}

	/**
	 * パラメタマップ取得
	 *
	 * @return パラメタマップ
	 */
	public Set<String> getParameterKeySet() {
		return this.param.keySet();
	}

	/**
	 * パラメタマップ設定
	 * @param uf 汎用フォーム
	 */
	public void setParameterTo(final UniModel uf) {
		setParameterTo(uf, Collections.emptySet());
	}

	/**
	 * パラメタマップ設定
	 * @param uf 汎用フォーム
	 * @param exclusion 対象外集合
	 */
	public void setParameterTo(final UniModel uf, final Set<String> exclusion) {
		if (uf != null) {
			for (final Entry<String, String[]> me : this.param.entrySet()) {
				if (ModelUtil.canSet(me.getKey())
								&& !UniModel.class.equals(uf.getValueClass(me.getKey()))
								&& !exclusion.contains(me.getKey())) {
					uf.setValue(me.getKey(), me.getValue());
				}
			}
		}
	}

	/**
	 * パラメタマップ存在確認
	 * @param key キー
	 * @return 存在した場合 true を返す。
	 */
	public boolean hasParameter(final String key) {
		return this.param.containsKey(key);
	}

	/**
	 * パラメタマップから値取得
	 *
	 * @param key キー
	 * @return 値
	 */
	public String getParameter(final String key) {
		String[] obj = this.param.get(key);
		if (obj != null && 0 < obj.length) {
			return obj[0];
		}
		this.param.put(key, new String[0]);
		return null;
	}

	/**
	 * パラメタマップから値取得
	 *
	 * @param key キー
	 * @return 値
	 */
	public String[] getArrayParameter(final String key) {
		String[] obj = this.param.get(key);
		if (obj != null) {
			return obj.clone();
		}
		String[] ret = {};
		this.param.put(key, ret);
		return ret;
	}

	/**
	 * パラメタマップに値設定（存在しない場合のみ）
	 * @param key キー
	 * @param val 値
	 */
	public void setParameter(final String key, final String... val) {
		if (this.param.get(key) == null) {
			this.param.put(key, ArrayUtil.copyOf(val));
		}
	}

	/**
	 * 項目削除
	 *
	 * @param key キー
	 */
	private void removeParameter(final String key) {
		this.param.remove(key);
	}

	/**
	 * 共通項目設定
	 */
	private void setCommon() {
		// プロパティ設定
		for (final Map.Entry<String, String> me : this.comm.entrySet()) {
			this.um.setValue(me.getKey(), me.getValue());
		}
	}

	/**
	 * 共通項目設定
	 * @return 共通項目
	 */
	public Map<String, String> getProperty() {
		return this.comm;
	}

	/**
	 * 保存アクションパラメタを現在のパラメタに設定
	 *
	 */
	private void setPreviousParameterMap() {
		if (this.store.getParameterMap() != null) {
			this.param.clear();
			this.param.putAll(this.store.getParameterMap());
		}
	}

	/**
	 * 画面表示用に元へ戻す。
	 * @param item ロールバックモデル項目名
	 */
	private void setPreviousModel(final String item) {
		// 現モデル保存項目削除
		Set<String> set = this.um.getReservedKeySet();
		if (set != null) {
			set.clear();
		}

		if (this.store.getUniModel() == null || !copyModelValue(
						this.store.getUniModel().getModel(item), this.um.getModel(item))) {
			copyModelValue(this.store.getUniModel(), this.um);
		}
	}

	/**
	 * モデル項目複写
	 * @param from 元モデル
	 * @param to 先モデル
	 * @return ロールバックした場合 true を返す。
	 */
	private boolean copyModelValue(final UniModel from, final UniModel to) {
		if (from == null || to == null) {
			return false;
		}

		// モデルクリア
		for (final Iterator<String> it = to.keySet().iterator(); it.hasNext();) {
			if (ModelUtil.canSet(it.next())) {
				it.remove();
			}
		}

		for (final String key : from.keySet()) {
			if (ModelUtil.canSet(key)) {
				setOptionParameter(key);
				from.copyValueTo(to, key);
			}
		}
		return true;
	}

	/**
	 * チェックボックス・ラジオボタン設定
	 *
	 * @param key キー
	 */
	private void setOptionParameter(final String key) {
		if (key.startsWith("chk") || key.startsWith("rdo")) {
			if (!this.param.containsKey(key)) {
				this.param.put(key, new String[0]);
			}
		}
	}

	/**
	 * アクションID設定
	 * @param map パラメタマップ
	 * @param method メソッド名
	 * @return アクションID
	 */
	private String getAidFromParam(final Map<String, String[]> map, final String method) {
		if (!FilterUtil.isGetMethod(method)) {
			// リクエストパラメタ項目名取得
			String[] val = map.get("AID");
			if (val != null && 0 < val.length && !Objects.toString(val[0], "").isEmpty()) {
				return val[0].trim();
			}
		}
		return method.toLowerCase(Locale.ENGLISH);
	}

	/**
	 * 保存値再保存処理
	 *
	 */
	private void rereserve() {
		if (this.store.getUniModel() != null) {
			for (final String key : this.store.getUniModel().getReservedKeySet()) {
				if (ModelUtil.canSet(key)) {
					if (this.um.containsKey(key)) {
						this.um.reserve(key);
					} else if (!this.actionSession) {
						this.store.getUniModel().copyValueTo(this.um, key);
						this.um.reserve(key);
					}
				}
			}
		}
	}

	/**
	 * セション保存クラス
	 * @author Tadashi Nakayama
	 * @version 1.0.0
	 */
	private static final class SessionStore implements Serializable {
		/** serialVersionUID */
		private static final long serialVersionUID = 319843050355600169L;

		/** 汎用モデル */
		private final UniModel model;
		/** パラメタマップ */
		private final Map<String, String[]> map;
		/** ロールバックフラグ */
		private final boolean rb;
		/** クエリ */
		private final String qry;
		/** 画面ID */
		private final String gid;
		/** トークン */
		private final String token;

		/**
		 * コンストラクタ
		 * @param um 汎用モデル
		 * @param param パラメタマップ
		 * @param rollback ロールバックフラグ
		 * @param qs クエリ文字列
		 * @param gamen 画面ID
		 * @param tk トークン
		 */
		public SessionStore(final UniModel um,
						final Map<String, String[]> param, final boolean rollback,
						final String qs, final String gamen, final String tk) {
			this.model = um;
			this.rb = rollback;
			this.qry = qs;
			this.gid = gamen;
			this.token = tk;
			if (param != null) {
				this.map = new HashMap<>(param);
			} else {
				this.map = null;
			}
		}

		/**
		 * 汎用モデル取得
		 * @return 汎用モデル
		 */
		public UniModel getUniModel() {
			return this.model;
		}

		/**
		 * パラメタマップ取得
		 * @return パラメタマップ
		 */
		public Map<String, String[]> getParameterMap() {
			return this.map;
		}

		/**
		 * ロールバックフラグ取得
		 * @return ロールバックフラグ
		 */
		public boolean isRollback() {
			return this.rb;
		}

		/**
		 * クエリ取得
		 * @return クエリ
		 */
		public String getQueryString() {
			return this.qry;
		}

		/**
		 * 画面ID取得
		 * @return 画面ID
		 */
		public String getGid() {
			return this.gid;
		}

		/**
		 * トークン取得
		 * @return トークン
		 */
		public String getToken() {
			return this.token;
		}
	}
}
